/******************************************************************************
 * Copyright (C) 2015  671643387@qq.com
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 *****************************************************************************/

#include "Game/Game/SQLStorages.h"
#include <set>
#include <algorithm>
#include "Shared/Log/Log.h"
#include "Shared/DB/SQLStorageImpl.h"
#include "Game/Global.h"
#include "Game/Game/SQLStorageFmt.h"
#include "Game/Game/Map.h"
#include "Game/Game/DBCStores.h"
#include "Game/Game/MapManager.h"
#include "Game/Game/Bag.h"
#include "Game/Game/NPCHandler.h"
#include "Game/Game/ItemEnchantmentMgr.h"
#include "Game/World/World.h"
using namespace Shared::DB;

SQLStorages *SQLStorages::_instance = NULL;

SQLStorages *SQLStorages::instance(void)
{
    SHARED_ASSERT(_instance);
    return _instance;
}

SQLStorages::SQLStorages(void)
    : _instanceTemplate(InstanceTemplatesrcfmt, InstanceTemplatedstfmt, "map", "instance_template")
    , _itemStorage(ItemPrototypesrcfmt, ItemPrototypedstfmt, "entry", "item_template")
    , _pageTextStore(PageTextfmt, "entry", "page_text")
{
    SHARED_ASSERT(!_instance);
    _instance = this;
}

SQLStorages::~SQLStorages(void)
{

}


struct SQLInstanceLoader : public SQLStorageLoaderBase<SQLInstanceLoader, SQLStorage>
{
    template<class D>
    void convert_from_str(std::uint32_t /*field_pos*/, char const* src, D& dst)
    {
        // TODO
        //dst = D(sScriptMgr.GetScriptId(src));
        dst = D(0);
    }
};
bool SQLStorages::loadInstanceTemplate(void)
{
    SQLInstanceLoader loader;
    loader.load(gameSystem.worldDB(), _instanceTemplate);

    for (std::uint32_t i = 0; i < _instanceTemplate.getMaxEntry(); ++i)
    {
        const InstanceTemplate* temp = _instanceTemplate.lookupEntry<InstanceTemplate>(i);
        if (!temp)
        { continue; }

        const MapEntry* mapEntry = dbcStores.mapStore().lookupEntry(temp->Map);
        if (!mapEntry)
        {
            sharedLog.outDbError("ObjectMgr::LoadInstanceTemplate: bad mapid %d for template!", temp->Map);
            _instanceTemplate.eraseEntry(i);
            continue;
        }

        if (!mapEntry->instanceable())
        {
            sharedLog.outDbError("ObjectMgr::LoadInstanceTemplate: non-instanceable mapid %d for template!", temp->Map);
            _instanceTemplate.eraseEntry(i);
            continue;
        }

        if (temp->Parent > 0)
        {
            // check existence
            MapEntry const* parentEntry = dbcStores.mapStore().lookupEntry(temp->Parent);
            if (!parentEntry)
            {
                sharedLog.outDbError("ObjectMgr::LoadInstanceTemplate: bad parent map id %u for instance template %d template!",
                    temp->Parent, temp->Map);
                const_cast<InstanceTemplate*>(temp)->Parent = 0;
                continue;
            }

            if (parentEntry->isContinent())
            {
                sharedLog.outDbError("ObjectMgr::LoadInstanceTemplate: parent point to continent map id %u for instance template %d template, ignored, need be set only for non-continent parents!",
                    parentEntry->MapID, temp->Map);
                const_cast<InstanceTemplate*>(temp)->Parent = 0;
                continue;
            }
        }

        // if ghost entrance coordinates provided, can't be not exist for instance without ground entrance
        if (temp->GhostEntranceMap >= 0)
        {
            if (!MapManager::isValidMapCoord(temp->GhostEntranceMap, temp->GhostEntranceX, temp->GhostEntranceY))
            {
                sharedLog.outDbError("ObjectMgr::LoadInstanceTemplate: ghost entrance coordinates invalid for instance template %d template, ignored, need be set only for non-continent parents!", temp->Map);
                _instanceTemplate.eraseEntry(i);
                continue;
            }

            MapEntry const* ghostEntry = dbcStores.mapStore().lookupEntry(temp->GhostEntranceMap);
            if (!ghostEntry)
            {
                sharedLog.outDbError("ObjectMgr::LoadInstanceTemplate: bad ghost entrance map id %u for instance template %d template!", temp->GhostEntranceMap, temp->Map);
                _instanceTemplate.eraseEntry(i);
                continue;
            }

            if (!ghostEntry->isContinent())
            {
                sharedLog.outDbError("ObjectMgr::LoadInstanceTemplate: ghost entrance not at continent map id %u for instance template %d template, ignored, need be set only for non-continent parents!", ghostEntry->MapID, temp->Map);
                _instanceTemplate.eraseEntry(i);
                continue;
            }
        }

        // the reset_delay must be at least one day
        if (temp->ResetDelay) { const_cast<InstanceTemplate*>(temp)->ResetDelay = SHARED_MAX((std::uint32_t)1, (std::uint32_t)(temp->ResetDelay * gameWorld.getConfig(CONFIG_FLOAT_RATE_INSTANCE_RESET_TIME))); }
    }

    sharedLog.outString(">> Loaded %u Instance Template definitions", _instanceTemplate.getRecordCount());
    sharedLog.outString();

    return true;
}

struct SQLItemLoader : public SQLStorageLoaderBase<SQLItemLoader, SQLStorage>
{
    template<class D>
    void convert_from_str(std::uint32_t /*field_pos*/, char const* src, D& dst)
    {
        // TODO
        //dst = D(sScriptMgr.GetScriptId(src));
        dst = D(0);
    }
};
bool SQLStorages::loadItemPrototypes(void)
{
    SQLItemLoader loader;
    loader.load(gameSystem.worldDB(), _itemStorage);
    sharedLog.outString(">> Loaded %u item prototypes", _itemStorage.getRecordCount());
    sharedLog.outString();

    // check data correctness
    for (std::uint32_t i = 1; i < _itemStorage.getMaxEntry(); ++i)
    {
        ItemPrototype const* proto = _itemStorage.lookupEntry<ItemPrototype >(i);
        if (!proto)
        { continue; }

        if (proto->Class >= MAX_ITEM_CLASS)
        {
            sharedLog.outDbError("Item (Entry: %u) has wrong Class value (%u)", i, proto->Class);
            const_cast<ItemPrototype*>(proto)->Class = ITEM_CLASS_MISC;
        }

        if (proto->SubClass >= MaxItemSubclassValues[proto->Class])
        {
            sharedLog.outDbError("Item (Entry: %u) has wrong Subclass value (%u) for class %u", i, proto->SubClass, proto->Class);
            const_cast<ItemPrototype*>(proto)->SubClass = 0;// exist for all item classes
        }

        if (proto->Quality >= MAX_ITEM_QUALITY)
        {
            sharedLog.outDbError("Item (Entry: %u) has wrong Quality value (%u)", i, proto->Quality);
            const_cast<ItemPrototype*>(proto)->Quality = ITEM_QUALITY_NORMAL;
        }

        if (proto->BuyCount <= 0)
        {
            sharedLog.outDbError("Item (Entry: %u) has wrong BuyCount value (%u), set to default(1).", i, proto->BuyCount);
            const_cast<ItemPrototype*>(proto)->BuyCount = 1;
        }

        if (proto->InventoryType >= MAX_INVTYPE)
        {
            sharedLog.outDbError("Item (Entry: %u) has wrong InventoryType value (%u)", i, proto->InventoryType);
            const_cast<ItemPrototype*>(proto)->InventoryType = INVTYPE_NON_EQUIP;
        }

        if (proto->InventoryType != INVTYPE_NON_EQUIP)
        {
            if (proto->Flags & ITEM_FLAG_LOOTABLE)
            {
                sharedLog.outDbError("Item container (Entry: %u) has not allowed for containers flag ITEM_FLAG_LOOTABLE (%u), flag removed.", i, ITEM_FLAG_LOOTABLE);
                const_cast<ItemPrototype*>(proto)->Flags &= ~ITEM_FLAG_LOOTABLE;
            }
        }
        else if (proto->InventoryType != INVTYPE_BAG)
        {
            if (proto->ContainerSlots > 0)
            {
                sharedLog.outDbError("Non-container item (Entry: %u) has ContainerSlots (%u), set to 0.", i, proto->ContainerSlots);
                const_cast<ItemPrototype*>(proto)->ContainerSlots = 0;
            }
        }

        if (proto->RequiredSkill >= MAX_SKILL_TYPE)
        {
            sharedLog.outDbError("Item (Entry: %u) has wrong RequiredSkill value (%u)", i, proto->RequiredSkill);
            const_cast<ItemPrototype*>(proto)->RequiredSkill = 0;
        }

        {
            // can be used in equip slot, as page read use in inventory, or spell casting at use
            bool req = proto->InventoryType != INVTYPE_NON_EQUIP || proto->PageText;
            if (!req)
            {
                for (int j = 0; j < MAX_ITEM_PROTO_SPELLS; ++j)
                {
                    if (proto->Spells[j].SpellId)
                    {
                        req = true;
                        break;
                    }
                }
            }

            if (req)
            {
                if (!(proto->AllowableClass & CLASSMASK_ALL_PLAYABLE))
                { sharedLog.outDbError("Item (Entry: %u) not have in `AllowableClass` any playable classes (%u) and can't be equipped or use.", i, proto->AllowableClass); }

                if (!(proto->AllowableRace & RACEMASK_ALL_PLAYABLE))
                { sharedLog.outDbError("Item (Entry: %u) not have in `AllowableRace` any playable races (%u) and can't be equipped or use.", i, proto->AllowableRace); }
            }
        }

        if (proto->RequiredSpell && !dbcStores.spellStore().lookupEntry(proto->RequiredSpell))
        {
            sharedLog.outDbError("Item (Entry: %u) have wrong (nonexistent) spell in RequiredSpell (%u)", i, proto->RequiredSpell);
            const_cast<ItemPrototype*>(proto)->RequiredSpell = 0;
        }

        if (proto->RequiredReputationRank >= MAX_REPUTATION_RANK)
        { sharedLog.outDbError("Item (Entry: %u) has wrong reputation rank in RequiredReputationRank (%u), item can't be used.", i, proto->RequiredReputationRank); }

        if (proto->RequiredReputationFaction)
        {
            if (!dbcStores.factionStore().lookupEntry(proto->RequiredReputationFaction))
            {
                sharedLog.outDbError("Item (Entry: %u) has wrong (not existing) faction in RequiredReputationFaction (%u)", i, proto->RequiredReputationFaction);
                const_cast<ItemPrototype*>(proto)->RequiredReputationFaction = 0;
            }

            if (proto->RequiredReputationRank == MIN_REPUTATION_RANK)
            { sharedLog.outDbError("Item (Entry: %u) has min. reputation rank in RequiredReputationRank (0) but RequiredReputationFaction > 0, faction setting is useless.", i); }
        }
        // else if(proto->RequiredReputationRank > MIN_REPUTATION_RANK)
        //    sharedLog.outDbError("Item (Entry: %u) has RequiredReputationFaction ==0 but RequiredReputationRank > 0, rank setting is useless.",i);

        if (proto->Stackable == 0)
        {
            sharedLog.outDbError("Item (Entry: %u) has wrong value in stackable (%u), replace by default 1.", i, proto->Stackable);
            const_cast<ItemPrototype*>(proto)->Stackable = 1;
        }
        else if (proto->Stackable > 255)
        {
            sharedLog.outDbError("Item (Entry: %u) has too large value in stackable (%u), replace by hardcoded upper limit (255).", i, proto->Stackable);
            const_cast<ItemPrototype*>(proto)->Stackable = 255;
        }

        if (proto->ContainerSlots)
        {
            if (proto->ContainerSlots > MAX_BAG_SIZE)
            {
                sharedLog.outDbError("Item (Entry: %u) has too large value in ContainerSlots (%u), replace by hardcoded limit (%u).", i, proto->ContainerSlots, MAX_BAG_SIZE);
                const_cast<ItemPrototype*>(proto)->ContainerSlots = MAX_BAG_SIZE;
            }
        }

        for (int j = 0; j < MAX_ITEM_PROTO_STATS; ++j)
        {
            // for ItemStatValue != 0
            if (proto->ItemStat[j].ItemStatValue && proto->ItemStat[j].ItemStatType >= MAX_ITEM_MOD)
            {
                sharedLog.outDbError("Item (Entry: %u) has wrong stat_type%d (%u)", i, j + 1, proto->ItemStat[j].ItemStatType);
                const_cast<ItemPrototype*>(proto)->ItemStat[j].ItemStatType = 0;
            }
        }

        for (int j = 0; j < MAX_ITEM_PROTO_DAMAGES; ++j)
        {
            if (proto->Damage[j].DamageType >= MAX_SPELL_SCHOOL)
            {
                sharedLog.outDbError("Item (Entry: %u) has wrong dmg_type%d (%u)", i, j + 1, proto->Damage[j].DamageType);
                const_cast<ItemPrototype*>(proto)->Damage[j].DamageType = 0;
            }
        }

        // normal spell list
        {
            for (int j = 0; j < MAX_ITEM_PROTO_SPELLS; ++j)
            {
                if (proto->Spells[j].SpellTrigger >= MAX_ITEM_SPELLTRIGGER)
                {
                    sharedLog.outDbError("Item (Entry: %u) has wrong item spell trigger value in spelltrigger_%d (%u)", i, j + 1, proto->Spells[j].SpellTrigger);
                    const_cast<ItemPrototype*>(proto)->Spells[j].SpellId = 0;
                    const_cast<ItemPrototype*>(proto)->Spells[j].SpellTrigger = ITEM_SPELLTRIGGER_ON_USE;
                }
                // on hit can be sued only at weapon
                else if (proto->Spells[j].SpellTrigger == ITEM_SPELLTRIGGER_CHANCE_ON_HIT)
                {
                    if (proto->Class != ITEM_CLASS_WEAPON)
                    { sharedLog.outDbError("Item (Entry: %u) isn't weapon (Class: %u) but has on hit spelltrigger_%d (%u), it will not triggered.", i, proto->Class, j + 1, proto->Spells[j].SpellTrigger); }
                }

                if (proto->Spells[j].SpellId)
                {
                    SpellEntry const* spellInfo = dbcStores.spellStore().lookupEntry(proto->Spells[j].SpellId);
                    if (!spellInfo)
                    {
                        sharedLog.outDbError("Item (Entry: %u) has wrong (not existing) spell in spellid_%d (%u)", i, j + 1, proto->Spells[j].SpellId);
                        const_cast<ItemPrototype*>(proto)->Spells[j].SpellId = 0;
                    }
                }
            }
        }

        if (proto->Bonding >= MAX_BIND_TYPE)
        { sharedLog.outDbError("Item (Entry: %u) has wrong Bonding value (%u)", i, proto->Bonding); }

        if (proto->PageText)
        {
            if (!_pageTextStore.lookupEntry<PageText>(proto->PageText))
            { sharedLog.outDbError("Item (Entry: %u) has non existing first page (Id:%u)", i, proto->PageText); }
        }

        if (proto->LockID && !dbcStores.lockStore().lookupEntry(proto->LockID))
        { sharedLog.outDbError("Item (Entry: %u) has wrong LockID (%u)", i, proto->LockID); }

        if (proto->Sheath >= MAX_SHEATHETYPE)
        {
            sharedLog.outDbError("Item (Entry: %u) has wrong Sheath (%u)", i, proto->Sheath);
            const_cast<ItemPrototype*>(proto)->Sheath = SHEATHETYPE_NONE;
        }

        // TODO
        //if (proto->RandomProperty && !dbcStores.itemRandomPropertiesStore().lookupEntry(getItemEnchantMod(proto->RandomProperty)))
        //{
        //    sharedLog.outDbError("Item (Entry: %u) has unknown (wrong or not listed in `item_enchantment_template`) RandomProperty (%u)", i, proto->RandomProperty);
        //    const_cast<ItemPrototype*>(proto)->RandomProperty = 0;
        //}
        //
        //if (proto->ItemSet && !sItemSetStore.LookupEntry(proto->ItemSet))
        //{
        //    sharedLog.outDbError("Item (Entry: %u) have wrong ItemSet (%u)", i, proto->ItemSet);
        //    const_cast<ItemPrototype*>(proto)->ItemSet = 0;
        //}
        //
        //if (proto->Area && !GetAreaEntryByAreaID(proto->Area))
        //{ sharedLog.outDbError("Item (Entry: %u) has wrong Area (%u)", i, proto->Area); }
        //
        //if (proto->Map && !dbcStores.mapStore().lookupEntry(proto->Map))
        //{ sharedLog.outDbError("Item (Entry: %u) has wrong Map (%u)", i, proto->Map); }
        //
        //if (proto->BagFamily)
        //{
        //    ItemBagFamilyEntry const* bf = sItemBagFamilyStore.LookupEntry(proto->BagFamily);
        //    if (!bf)
        //    {
        //        sharedLog.outDbError("Item (Entry: %u) has bag family %u not listed in ItemBagFamily.dbc, setted it to 0", i, proto->BagFamily);
        //        const_cast<ItemPrototype*>(proto)->BagFamily = 0;
        //    }
        //}

        if (proto->DisenchantID)
        {
            if (proto->Quality > ITEM_QUALITY_EPIC || proto->Quality < ITEM_QUALITY_UNCOMMON)
            {
                sharedLog.outDbError("Item (Entry: %u) has wrong quality (%u) for disenchanting, remove disenchanting loot id.", i, proto->Quality);
                const_cast<ItemPrototype*>(proto)->DisenchantID = 0;
            }
            else if (proto->Class != ITEM_CLASS_WEAPON && proto->Class != ITEM_CLASS_ARMOR)
            {
                sharedLog.outDbError("Item (Entry: %u) has wrong item class (%u) for disenchanting, remove disenchanting loot id.", i, proto->Class);
                const_cast<ItemPrototype*>(proto)->DisenchantID = 0;
            }
        }

        if (proto->FoodType >= MAX_PET_DIET)
        {
            sharedLog.outDbError("Item (Entry: %u) has wrong FoodType value (%u)", i, proto->FoodType);
            const_cast<ItemPrototype*>(proto)->FoodType = 0;
        }

        if (proto->ExtraFlags)
        {
            if (proto->ExtraFlags & ~ITEM_EXTRA_ALL)
            { sharedLog.outDbError("Item (Entry: %u) has wrong ExtraFlags (%u) with unused bits set", i, proto->ExtraFlags); }

            if (proto->ExtraFlags & ITEM_EXTRA_NON_CONSUMABLE)
            {
                bool can_be_need = false;
                for (int j = 0; j < MAX_ITEM_PROTO_SPELLS; ++j)
                {
                    if (proto->Spells[j].SpellCharges < 0)
                    {
                        can_be_need = true;
                        break;
                    }
                }

                if (!can_be_need)
                {
                    sharedLog.outDbError("Item (Entry: %u) has redundant non-consumable flag in ExtraFlags, item not have negative charges", i);
                    const_cast<ItemPrototype*>(proto)->ExtraFlags &= ~ITEM_EXTRA_NON_CONSUMABLE;
                }
            }

            if (proto->ExtraFlags & ITEM_EXTRA_REAL_TIME_DURATION)
            {
                if (proto->Duration == 0)
                {
                    sharedLog.outDbError("Item (Entry: %u) has redundant real-time duration flag in ExtraFlags, item not have duration", i);
                    const_cast<ItemPrototype*>(proto)->ExtraFlags &= ~ITEM_EXTRA_REAL_TIME_DURATION;
                }
            }
        }
    }

    // TODO
    // check some dbc referenced items (avoid duplicate reports)
    //std::set<std::uint32_t> notFoundOutfit;
    //for (std::uint32_t i = 1; i < sCharStartOutfitStore.GetNumRows(); ++i)
    //{
    //    CharStartOutfitEntry const* entry = sCharStartOutfitStore.LookupEntry(i);
    //    if (!entry)
    //    { continue; }
    //
    //    for (int j = 0; j < MAX_OUTFIT_ITEMS; ++j)
    //    {
    //        if (entry->ItemId[j] <= 0)
    //        { continue; }
    //
    //        std::uint32_t item_id = entry->ItemId[j];
    //
    //        if (!gameObjectMgr.getItemPrototype(item_id))
    //        { notFoundOutfit.insert(item_id); }
    //    }
    //}
    //
    //for (std::set<std::uint32_t>::const_iterator itr = notFoundOutfit.begin(); itr != notFoundOutfit.end(); ++itr)
    //{ sharedLog.outDbError("Item (Entry: %u) not exist in `item_template` but referenced in `CharStartOutfit.dbc`", *itr); }

    return true;
}

bool SQLStorages::loadPageTexts(void)
{
    _pageTextStore.load(gameSystem.worldDB());
    sharedLog.outString(">> Loaded %u page texts", _pageTextStore.getRecordCount());
    sharedLog.outString();

    for (std::uint32_t i = 1; i < _pageTextStore.getMaxEntry(); ++i)
    {
        // check data correctness
        PageText const* page = _pageTextStore.lookupEntry<PageText>(i);
        if (!page)
        { continue; }

        if (page->NextPage && !_pageTextStore.lookupEntry<PageText>(page->NextPage))
        {
            sharedLog.outDbError("Page text (Id: %u) has not existing next page (Id:%u)", i, page->NextPage);
            continue;
        }

        // detect circular reference
        std::set<std::uint32_t> checkedPages;
        for (PageText const* pageItr = page; pageItr; pageItr = _pageTextStore.lookupEntry<PageText>(pageItr->NextPage))
        {
            if (!pageItr->NextPage)
            { break; }
            checkedPages.insert(pageItr->PageID);
            if (checkedPages.find(pageItr->NextPage) != checkedPages.end())
            {
                std::ostringstream ss;
                ss << "The text page(s) ";
                for (std::set<std::uint32_t>::iterator itr = checkedPages.begin(); itr != checkedPages.end(); ++itr)
                { ss << *itr << " "; }
                ss << "create(s) a circular reference, which can cause the server to freeze. Changing Next_Page of page "
                    << pageItr->PageID << " to 0";
                sharedLog.outDbError("%s", ss.str().c_str());
                const_cast<PageText*>(pageItr)->NextPage = 0;
                break;
            }
        }
    }

    return true;
}

bool SQLStorages::load(void)
{
    do 
    {
        if (!loadInstanceTemplate()) {break;}
        if (!loadPageTexts()) { break; }
        if (!loadItemPrototypes()) {break;}
        return true;
    } while (false);
    return false;
}